Linux 互斥锁 读写锁 条件变量 信号量 (备查)

您所在的位置:网站首页 linux 信号量性能 Linux 互斥锁 读写锁 条件变量 信号量 (备查)

Linux 互斥锁 读写锁 条件变量 信号量 (备查)

2024-07-11 07:26| 来源: 网络整理| 查看: 265

线程同步

1)所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。 2)如没有对线程进行同步处理,会导致多个线程访问共享资源出现数据混乱的问题。 3)所谓共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源。

4)常用的线程同步方式有四种:互斥锁、读写锁、条件变量、信号量。

5)通过锁机制能保证临界区代码最多只能同时有一个线程访问,这样并行访问就变为串行访问了。

互斥锁

1)通过互斥锁可以锁定一个代码块, 被锁定的这个代码块, 所有的线程只能顺序执行(不能并行处理),需要付出的代价就是执行效率的降低。

1)一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。 2)一般情况下,每一个共享资源对应一把互斥锁。

创建互斥锁 //在Linux中互斥锁的类型为pthread_mutex_t,创建一个这种类型的变量就得到了一把互斥锁: pthread_mutex_t mutex; // 初始化互斥锁 // restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 参数: mutex: 互斥锁变量的地址 attr: 互斥锁的属性, 一般使用默认属性即可, 这个参数指定为NULL //函数调用成功会返回0,调用失败会返回相应的错误号: 加锁 // 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中 int pthread_mutex_lock(pthread_mutex_t *mutex);

这个函数被调用, 首先会判断参数 mutex 互斥锁中的状态是不是锁定状态 1)如果被锁定了, 其他线程加锁就失败了, 这些线程都会阻塞在这把锁上。 2)当这把锁被解开之后, 这些阻塞在锁上的线程就解除阻塞。 3)没抢到锁的线程继续阻塞。

// 尝试加锁 int pthread_mutex_trylock(pthread_mutex_t *mutex);

调用这个函数对互斥锁变量加锁还是有两种情况: 1)如果这把锁没有被锁定是打开的,线程加锁成功 2)如果锁变量被锁住了,调用这个函数加锁的线程,不会被阻塞,加锁失败直接返回错误号

解锁 // 对互斥锁解锁 int pthread_mutex_unlock(pthread_mutex_t *mutex);

1)不是所有的线程都可以对互斥锁解锁,哪个线程加的锁, 哪个线程才能解锁成功。

释放互斥锁 // 释放互斥锁资源 int pthread_mutex_destroy(pthread_mutex_t *mutex); //函数调用成功会返回0,调用失败会返回相应的错误号: 死锁

1)锁使用不当, 就会造成死锁现象 2)线程死锁造成的后果是:使用这个共享资源的线程都被阻塞

造成死锁的场景有如下几种: 1)加锁之后忘记解锁。 2)重复加锁, 只解锁一次,造成死锁。 3)有多个共享资源, 两个线程交叉锁定了另一个线程的必要资源。

读写锁

读锁是共享的。写锁是独占的。

1)读写锁是互斥锁的升级版, 在做读操作的时候可以提高程序的执行效率。 2)读写锁是一把锁,可以做写锁定和读锁定两件事。 3)写锁的优先级高于****读锁。

创建读写锁 //在Linux中读写锁的类型为pthread_rwlock_t,创建一个这种类型的变量就得到了一把读写锁 pthread_rwlock_t rwlock;

在这把锁中大致记录了这些信息: 1)锁的状态: 锁定/打开。 2)锁定的是什么操作: 读操作/写操作,使用读写锁锁定了读操作,需要先解锁才能去锁定写操作。 3)哪个线程将这把锁锁上了。

// 初始化读写锁 int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); 参数: rwlock: 读写锁的地址,传出参数 attr: 读写锁属性,一般使用默认属性,指定为NULL 加读锁 // 对读写锁加读锁, 锁定的是读操作 int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 尝试加读锁,调用这个函数加锁失败,对应的线程不会被阻塞,这个函数可以有效的避免死锁。 int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); // 如果加读锁失败, 不会阻塞当前线程, 直接返回错误号 加写锁 // 对读写锁加写锁, 锁定的是写操作 int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数的线程会被阻塞。

// // 尝试加写锁,调用这个函数加锁失败,对应的线程不会被阻塞,这个函数可以有效的避免死锁。 int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); // 如果加写锁失败, 不会阻塞当前线程, 直接返回错误号 //可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。 解锁 // 解锁, 不管锁定了读还是写都可用解锁 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); 释放读写锁 // 释放读写锁占用的系统资源 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); 其他

如果说程序中所有的线程都对共享资源做写操作,使用读写锁没有优势,和互斥锁是一样的, 如果说程序中所有的线程都对共享资源有写也有读操作,并且对共享资源读的操作越多,读写锁更有优势。

条件变量

1)条件变量的主要作用不是处理线程同步, 而是进行线程的阻塞。 2)一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。

创建条件变量 //条件变量类型为 pthread_cond_t,这样就定义一个条件变量类型的变量了 pthread_cond_t cond;

被条件变量阻塞的线程的线程信息会被记录到这个变量中,以便在解除阻塞的时候使用。

// 初始化条件变量 int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); 参数: cond: 条件变量的地址 attr: 条件变量属性, 一般使用默认属性, 指定为NULL 阻塞线程 // 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞 int pthread_cond_wait(pthread_cond_t *restrict cond, //条件变量 pthread_mutex_t *restrict mutex); //互斥锁

在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步

该函数会对这个互斥锁做以下几件事情: 1)在阻塞线程时候,如果线程已经对互斥锁mutex上锁,那么会将这把锁打开,这样做是为了避免死锁 2)当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个mutex互斥锁锁上,继续向下访问临界区

// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示 struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds [0 .. 999999999] */ }; // 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了 int pthread_cond_timedwait(pthread_cond_t *restrict cond, //条件变量 pthread_mutex_t *restrict mutex, //互斥锁 const struct timespec *restrict abstime);//表示线程阻塞的时长

struct timespec这个结构体中记录的时间是从1971.1.1到某个时间点的时间,总长度使用秒/纳秒表示。

time_t mytim = time(NULL); // 1970.1.1 0:0:0 到当前的总秒数 struct timespec tmsp; tmsp.tv_sec = time(NULL) + 100; // 线程阻塞100s tmsp.tv_nsec = 0;//初始化为0,避免出错 唤醒线程 // 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞 int pthread_cond_signal(pthread_cond_t *cond); // 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞 int pthread_cond_broadcast(pthread_cond_t *cond);

1)调用上面两个函数中的任意一个,都可以唤醒被 pthread_cond_wait 或者 pthread_cond_timedwait 阻塞的线程, 2)区别就在于 pthread_cond_signal 是唤醒至少一个被阻塞的线程(总个数不定), pthread_cond_broadcast 是唤醒所有被阻塞的线程。

释放条件变量 // 销毁释放资源 int pthread_cond_destroy(pthread_cond_t *cond); 信号量 创建信号量 //信号的类型为sem_t对应的头文件为 #include sem_t sem; // 初始化信号量/信号灯 int sem_init(sem_t *sem, int pshared, unsigned int value); 参数: sem:信号量变量地址 pshared: 0:线程同步 非0:进程同步 value:初始化当前信号量拥有的资源数(>=0),如果资源数为0,线程就会被阻塞了。 判断 是否阻塞 // 函数被调用sem中的资源就会被消耗1个, 资源数-1 int sem_wait(sem_t *sem); //阻塞

1)sem中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数-1, 2)直到sem中的资源数减为0时,资源被耗尽,因此线程也就被阻塞了。

// 函数被调用sem中的资源就会被消耗1个, 资源数-1 int sem_trywait(sem_t *sem); //不阻塞

1)并且sem中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数-1, 2)直到sem中的资源数减为0时,资源被耗尽,但是线程不会被阻塞,直接返回错误号, 3)因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。

// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示 struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds [0 .. 999999999] */ }; // 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞。 // abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的 int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

1)sem中的资源数>0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数-1, 2)直到sem中的资源数减为0时,资源被耗尽,线程被阻塞, 3)当阻塞指定的 abs_timeout 时长之后,线程解除阻塞。

唤醒阻塞 线程 // 调用该函数给sem中的资源数+1 int sem_post(sem_t *sem);

1)该函数会将sem中的资源数+1, 2)如果有线程在调用 sem_wait、sem_trywait、sem_timedwait 时因为sem中的资源数为0被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行。

获取现有可用资源数 // 查看信号量 sem 中的整形数的当前值, 这个值会被写入到sval指针对应的内存中 int sem_getvalue(sem_t *sem, int *sval); // sval是一个传出参数

1)这个函数可以查看sem中现在拥有的资源个数,通过第二个参数 sval 将数据传出, 2)也就是说第二个参数的作用和返回值是一样的。

释放信号量 // 参数 sem 就是 sem_init() 的第一个参数 int sem_destroy(sem_t *sem); // 资源释放, 线程销毁之后调用这个函数即可 其他 生产者和消费者 模型+案列

在这里插入图片描述

-------------------生产者消费者 案列(使用条件变量)----------------------------- #include #include #include #include #include // 链表的节点 struct Node { int number; struct Node* next; }; // 定义条件变量, 控制消费者线程 pthread_cond_t cond; // 互斥锁变量 pthread_mutex_t mutex; // 指向头结点的指针 struct Node * head = NULL; // 生产者的回调函数 void* producer(void* arg) { // 一直生产 while(1) { pthread_mutex_lock(&mutex); // 创建一个链表的新节点 struct Node* pnew = (struct Node*)malloc(sizeof(struct Node)); // 节点初始化 pnew->number = rand() % 1000; // 节点的连接, 添加到链表的头部, 新节点就新的头结点 pnew->next = head; // head指针前移 head = pnew; printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self()); pthread_mutex_unlock(&mutex); // 生产了任务, 通知消费者消费 pthread_cond_broadcast(&cond); // 生产慢一点 sleep(rand() % 3); } return NULL; } // 消费者的回调函数 void* consumer(void* arg) { while(1) { pthread_mutex_lock(&mutex); // 一直消费, 删除链表中的一个节点 // if(head == NULL) // 这样写有bug while(head == NULL) { // 这函数会自动将线程拥有的锁解开 pthread_cond_wait(&cond, &mutex); // 当消费者线程解除阻塞之后, 会自动将这把锁锁上 // 这时候当前这个线程又重新拥有了这把互斥锁 } // 取出链表的头结点, 将其删除 struct Node* pnode = head; printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self()); head = pnode->next; free(pnode); pthread_mutex_unlock(&mutex); sleep(rand() % 3); } return NULL; } int main() { // 初始化条件变量 pthread_cond_init(&cond, NULL); pthread_mutex_init(&mutex, NULL); // 创建5个生产者, 5个消费者 pthread_t ptid[5]; pthread_t ctid[5]; for(int i=0; inumber, pthread_self()); head = pnode->next; free(pnode); // 通知生产者生成, 给生产者加信号灯 sem_post(&psem); sleep(rand() % 3); } return NULL; } int main() { // 初始化信号量 // 生产者和消费者拥有的信号灯的总和为1 sem_init(&psem, 0, 1); // 生成者线程一共有1个信号灯 sem_init(&csem, 0, 0); // 消费者线程一共有0个信号灯 // 创建5个生产者, 5个消费者 pthread_t ptid[5]; pthread_t ctid[5]; for(int i=0; i


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3